﻿namespace Hippo.Console.Lib.Services
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using System.ServiceModel;
    using System.ServiceModel.Discovery;
    using System.Threading;
    using System.Threading.Tasks;
    using Hippo.Common.Extensions;

    /// <summary>
    /// For handling the connection with our agents
    /// </summary>
    public class ControlServicePanel : IDisposable
    {
        private readonly List<IControlServiceCallback> agents;
        private ServiceHost host;
        private bool disposed;

        /// <summary>
        /// Initializes a new instance of the <see cref="ControlServicePanel"/> class.
        /// </summary>
        /// <param name="serviceHost">A service host for the ControlService service type.</param>
        public ControlServicePanel(ServiceHost serviceHost)
        {
            agents = new List<IControlServiceCallback>();
            host = serviceHost;
        }

        ~ControlServicePanel()
        {
            Dispose(false);
        }

        public ReadOnlyCollection<IControlServiceCallback> Agents
        {
            get { return new ReadOnlyCollection<IControlServiceCallback>(agents); }
        }

        /// <summary>
        /// Opens the server by opening the port and broadcasting the UDP multicast announcement.
        /// Starts listening for incoming agents.
        /// </summary>
        public void Open()
        {
            // ** DISCOVERY ** //
            // make the service discoverable by adding the discovery behavior
            var discoveryBehavior = new ServiceDiscoveryBehavior();
            host.Description.Behaviors.Add(discoveryBehavior);

            // send announcements on UDP multicast transport
            discoveryBehavior.AnnouncementEndpoints.Add(new UdpAnnouncementEndpoint());

            // ** DISCOVERY ** //
            // add the discovery endpoint that specifies where to publish the services
            host.AddServiceEndpoint(new UdpDiscoveryEndpoint());

            host.Open();
            Console.Out.WriteLine("Opened server broadcasting UDP announcement");
        }

        /// <summary>
        /// Wait for agents with specified names. Will block the thread until they've connected or the timeout has passed.
        /// </summary>
        /// <remarks>
        /// NOTE This will break if any agent has connected before we start waiting
        /// </remarks>
        /// <param name="expectedAgents">The expected agents.</param>
        /// <param name="timeout">The timeout, when to stop waiting and admit the failure.</param>
        /// <returns>True if agents are connected, false if timout was triggered</returns>
        public bool WaitForAgents(string[] expectedAgents, TimeSpan timeout)
        {
            var connectedAgents = new List<string>();
            var agentsConnectedTrigger = new AutoResetEvent(false);
            ((ControlService)host.SingletonInstance).AgentRegistered += (sender, agentInfo) =>
            {
                connectedAgents.Add(agentInfo.ClientName);
                agents.Add(agentInfo.CallbackChannel);

                /* Same agents */
                if (connectedAgents.Union(expectedAgents).Count() == expectedAgents.Length)
                {
                    agentsConnectedTrigger.Set();
                }
            };

            Console.Out.WriteLine("Waiting for agents {0} to connect", expectedAgents.ToDelimitedString(", "));
            return WaitHandle.WaitAll(new[] { agentsConnectedTrigger }, timeout);
        }

        /// <summary>
        /// Perform a task on connected agents
        /// </summary>
        /// <param name="call">Task to perform on connected agents.</param>
        public void Do(Action<IControlServiceCallback> call)
        {
            Parallel.For(0, agents.Count, i => call(agents[i]));
        }

        #region IDisposable
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (disposed)
            {
                return;
            }

            /* Managed */
            if (disposing)
            {
            }

            /* Unmanaged */
            if (host != null)
            {
                if (host.State == CommunicationState.Opened)
                {
                    host.Close();
                }

                host = null;
            }

            disposed = true;
        }
        #endregion
    }
}
